﻿//
// Author: Eldar Radovici
// Email: support@MvcDataTables.NET
// Website: http://MvcDataTables.NET
// Source: http://MvcDataTables.CodePlex.com/
// License: http://MvcDataTables.CodePlex.com/license
// 
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web.Mvc;

namespace MvcDataTables
{
    public class DataTableModel
    {
        #region Private fields
        private readonly UrlHelper _urlHelper;
        private readonly Type _controllerType;
        private readonly Type _enumeratedType;
        private readonly string _id;
        private readonly PropertyInfo[] _columns;
        private readonly string _getAction;
        #endregion

        public DataTableModel(UrlHelper urlHelper, Type controllerType, Type enumeratedType, string getAction)
        {
            _id = Guid.NewGuid().ToString();
            _urlHelper = urlHelper;
            _controllerType = controllerType;
            if (typeof(IDictionary<string, object>).IsAssignableFrom(enumeratedType))
            {
                _enumeratedType = enumeratedType;
                _columns = enumeratedType.GetProperties();
            }
            else
            {
                _enumeratedType = enumeratedType;
                _columns = enumeratedType.GetProperties();                
            }
            _getAction = getAction;
        }

        public string Id { get { return _id; } }

        public string Controller { get { return _controllerType.Name.Substring(0, _controllerType.Name.LastIndexOf("Controller", StringComparison.CurrentCultureIgnoreCase)); } }

        public string EnumeratedName { get { return _enumeratedType != null ? _enumeratedType.Name : string.Empty; } }

        public bool IsAddable { get { return !string.IsNullOrEmpty(AddAction); } }
        public bool IsEditable { get { return !string.IsNullOrEmpty(UpdateAction); } }
        public bool IsMasterDetail { get { return EnumerableColumns.Count() != 0; } }

        private IEnumerable<PropertyInfo> EnumerableColumns { get { return _columns.Where(lmb => DataTableExtensions.IsEnumerable(lmb.PropertyType)); } }

        public string GetAction { get { return _getAction; } }
        public string GetSource { get { return GetSourceOrDefault(GetAction); } }
        public string UpdateAction { get { return FindAction(ActionType.Update); } }
        public string UpdateSource { get { return GetSourceOrDefault(UpdateAction); } }
        public string AddAction { get { return FindAction(ActionType.Add); } }
        public string AddSource { get { return GetSourceOrDefault(AddAction); } }
        public string DeleteAction { get { return FindAction(ActionType.Delete); } }
        public string DeleteSource { get { return GetSourceOrDefault(DeleteAction); } }

        private string GetSourceOrDefault(string action)
        {
            return string.IsNullOrEmpty(action) ? string.Empty : _urlHelper.Action(action, Controller);
        }

        private enum ActionType
        {
            Get, Update, Add, Delete
        }

        private string FindAction(ActionType actionType)
        {
            return FindAction(actionType, _enumeratedType);
        }

        private string FindAction(ActionType actionType, Type enumeratedType, Type parameterType = null)
        {
            IEnumerable<MethodInfo> methodInfos = _controllerType.GetMethods();
            switch(actionType)
            {
                case ActionType.Get:
                    methodInfos = methodInfos.Where(lmb => typeof(JsonResult).IsAssignableFrom(lmb.ReturnType)
                            && lmb.ReturnType.GetGenericArguments().Length == 1
                            && lmb.ReturnType.GetGenericArguments()[0] == enumeratedType
                            && lmb.GetParameters().Count() == 2
                            && lmb.GetParameters()[0].ParameterType == typeof(DataTableViewModel)
                            && lmb.GetParameters()[1].ParameterType == parameterType);
                    break;
                case ActionType.Update:
                    //methodInfos = methodInfos.Where(lmb => lmb.GetParameters().Count() == 6); //editable datatables requires six parameters (good enough seek)
                    methodInfos = methodInfos.Where(lmb => lmb.GetParameters().Count() == 3
                        && lmb.GetParameters().Count(par => par.ParameterType == typeof(string)) == 2); //serialized enumerated type
                        //&& lmb.GetParameters().Count(par => par.ParameterType == enumeratedType) != 0); //NOTE (ER20130224): Tried adding another view model to better match the updated row (instead of just ID)
                    break;
                case ActionType.Add:
                    methodInfos = methodInfos.Where(lmb => lmb.GetParameters().Count() == 1 && lmb.GetParameters().Count(par => par.ParameterType == enumeratedType) != 0); //serialized enumerated type
                    break;
                case ActionType.Delete:
                    methodInfos = methodInfos.Where(lmb => lmb.GetParameters().Count() == 1 && lmb.GetParameters().Count(par => par.ParameterType == enumeratedType) != 0); //id key used to delete
                    break;
            }
            MethodInfo methodInfo = methodInfos.SingleOrDefault(lmb => lmb.Name == string.Format("{0}{1}", actionType, EnumeratedName));
            if (methodInfo == null)
            {
                methodInfo = methodInfos.FirstOrDefault(lmb => lmb.Name.StartsWith(actionType.ToString()));
            }
            string action = methodInfo != null ? methodInfo.Name : string.Empty;
            return action;
        }

        public string EditableHtml
        {
            get
            {
                string[] readableColumns = ReadableColumns;
                if (!readableColumns.Any()) return string.Empty;
                const string htmlTemplate =
@".makeEditable({{
    {0} //sUpdateURL: ""/Controller/UpdateData"",
    {1} //sAddURL: ""/Controller/AddData"",
    {2} //sDeleteURL: ""/Controller/DeleteData"",
    fnOnAdded: function (status) {{ table.fnReloadAjax(); }},
    //fnOnEdited: function (status) {{ }},
    fnOnDeleted: function (status) {{ table.fnReloadAjax(); }},
    ""aoColumns"": [{3}]
}})";
                const string htmlFieldTemplate =
@"{{
    indicator: ""Saving {0}..."",
    tooltip: ""Click to edit this {0}"",
}}";
                StringBuilder htmlFields = new StringBuilder();
                foreach (string column in readableColumns)
                {
                    htmlFields.Append(EditableColumns.Contains(column) ? string.Format(htmlFieldTemplate, column) : "null");
                    if (readableColumns.Last() != column)
                    {
                        htmlFields.AppendLine(",");
                    }
                }
                string html = string.Format(htmlTemplate, GetEditableUrl("sUpdateURL", UpdateSource), GetEditableUrl("sAddURL", AddSource), GetEditableUrl("sDeleteURL", DeleteSource), htmlFields);
                return html;
            }
        }

        private string GetEditableUrl(string action, string source)
        {
            return string.IsNullOrEmpty(source) ? string.Empty : string.Format("{0}: \"{1}\",", action, source);
        }

        public string MasterDetailJavascript
        {
            get
            {
                const string tableTemplate = "if (header == \"{0}\") {{ tableHtml = \"{1}\"; ajaxSource = \"/{2}/{4}\"; controller = \"/{2}\"; action = \"/{3}\"; }}";
                StringBuilder javascript = new StringBuilder();
                javascript.AppendLine("var tableHtml = \"\"");
                javascript.AppendLine("var ajaxSource = \"\"");
                javascript.AppendLine("var controller = \"\"");
                javascript.AppendLine("var action = \"\"");
                foreach (PropertyInfo propertyInfo in EnumerableColumns)
                {
                    Type genericParameterType = GetGenericOrDefault(propertyInfo.PropertyType);
                    string addAction = FindAction(ActionType.Add, genericParameterType);
                    string getAction = FindAction(ActionType.Get, genericParameterType, _enumeratedType);
                    string column = propertyInfo.Name;
                    string tableHtml = GetMasterDetailHtml(column);
                    if (string.IsNullOrEmpty(tableHtml)) continue;
                    javascript.AppendLine(string.Format(tableTemplate, column, tableHtml, Controller, addAction, getAction));
                }
                return javascript.ToString();
            }
        }

        private string GetMasterDetailHtml(string column)
        {
            PropertyInfo columnPropertyInfo = EnumerableColumns.SingleOrDefault(lmb => lmb.Name == column);
            if (columnPropertyInfo == null) return string.Empty;
            Type[] genericArguments = columnPropertyInfo.PropertyType.GetGenericArguments();
            if (genericArguments.Length == 0) return string.Empty;
            Type enumerableType = genericArguments[0];
            const string tableTemplate = "<table><thead><tr>{0}</tr></thead><tbody></tbody></table>";
            const string tableHeaderTemplate = "<th>{0}</th>";
            StringBuilder htmlFields = new StringBuilder();
            foreach (PropertyInfo propertyInfo in enumerableType.GetProperties().Where(lmb => lmb.CanRead))
            {
                htmlFields.Append(string.Format(tableHeaderTemplate, propertyInfo.Name));
            }
            string html = string.Format(tableTemplate, htmlFields);
            return html;
        }

        public string AddableHtml { get { return GetAddableHtml(WriteableColumns); } }

        private string GetAddableHtml(string[] writeableColumns, string id = "formAddNewRow")
        {
            if (!writeableColumns.Any()) return string.Empty;
            const string htmlTemplate =
@"<div>
    <form id=""{2}"" action=""#"" title=""Add {1}"">
        <fieldset>
            {0} 
        </fieldset>
    </form>
</div>";
            const string htmlFieldTemplate = @"<label for=""{0}"">{0}</label><input type=""text"" name=""{0}"" id=""{0}""/><br />";
            StringBuilder htmlFields = new StringBuilder();
            foreach (string column in writeableColumns)
            {
                htmlFields.AppendLine(string.Format(htmlFieldTemplate, column));
            }
            string html = string.Format(htmlTemplate, htmlFields, EnumeratedName, id);
            return html;
        }

        public string AddableHtmlForEnumeratedTypes
        {
            get
            {
                if (!EnumerableColumns.Any()) return string.Empty;
                const string HiddenJavascriptTemplate =
@"<script type=""text/javascript"">
    {0}
</script>";
                StringBuilder html = new StringBuilder();
                StringBuilder javascript = new StringBuilder();
                foreach (PropertyInfo propertyInfo in EnumerableColumns)
                {
                    html.AppendLine(GetAddableHtml(GetWriteableColumns(propertyInfo.PropertyType).Select(lmb => lmb.Name).ToArray(), propertyInfo.Name));
                    javascript.AppendLine(string.Format("$(\"#{0}\").hide();", propertyInfo.Name));
                }
                html.AppendLine(string.Format(HiddenJavascriptTemplate, javascript));
                return html.ToString();
            }
        }

        public string GetEditableClientsideFunctions
        {
            get
            {
                string template =
@"function SerializeRowToArray(table, tr, data) {{
    var enumeratedColumns = GetEnumeratedColumns();
    for (var i = 0; i < table.rows[0].cells.length; i++) {{
        var header = table.rows[0].cells[i].innerHTML;
        if ($.inArray(header, enumeratedColumns) == -1) {{
            data.push({{ ""name"": header, ""value"": tr.cells[i].editing ? tr.cells[i].revert : tr.cells[i].innerHTML }});
        }}
    }}
}}

function GetEnumeratedColumns() {{
    return([{0}]);
}}";
                StringBuilder enumeratedColumns = new StringBuilder();
                foreach (PropertyInfo propertyInfo in EnumerableColumns)
                {
                    if (enumeratedColumns.Length != 0) enumeratedColumns.Append(",");
                    enumeratedColumns.Append(string.Format("\"{0}\"", propertyInfo.Name));
                }
                string javascript = string.Format(template, enumeratedColumns);
                return javascript;
            }
        }

        public string[] ReadableColumns { get { return _columns.Where(lmb => lmb.CanRead).Select(lmb => lmb.Name).ToArray() ; } }
        public string[] WriteableColumns { get { return GetWriteableColumns(_columns).Select(lmb => lmb.Name).ToArray(); } }
        public string[] EditableColumns
        {
            get
            {
                IEnumerable<PropertyInfo> propertyInfos = _columns.Where(lmb => lmb.GetCustomAttributes(typeof(EditableColumnAttribute), false).Length != 0);
                if (!propertyInfos.Any())
                {
                    propertyInfos = GetWriteableColumns(_columns);
                }
                return propertyInfos.Select(lmb => lmb.Name).ToArray();
            }
        }

        private IEnumerable<PropertyInfo> GetWriteableColumns(Type type)
        {
            type = GetGenericOrDefault(type);
            return GetWriteableColumns(type.GetProperties());
        }

        private IEnumerable<PropertyInfo> GetWriteableColumns(IEnumerable<PropertyInfo> columns)
        {
            return columns.Where(lmb => lmb.CanWrite && !DataTableExtensions.IsEnumerable(lmb.PropertyType));
        }

        private Type GetGenericOrDefault(Type type)
        {
            if (typeof(IEnumerable).IsAssignableFrom(type))
            {
                type = type.GetGenericArguments()[0];
            }
            return type;
        }
    }
}